/* global module */
/**
 * @class MessageFormat
 * @classdesc see [messageformat.js](https://github.com/SlexAxton/messageformat.js)
 */
function MessageFormat(locale, pluralFunc) {
    var fallbackLocale;

    if (locale && pluralFunc) {
        MessageFormat.locale[locale] = pluralFunc;
    }

    // Defaults
    fallbackLocale = locale = locale || "en";
    pluralFunc = pluralFunc || MessageFormat.locale[fallbackLocale = MessageFormat.Utils.getFallbackLocale(locale)];

    if (!pluralFunc) {
        throw new Error("Plural Function not found for locale: " + locale);
    }

    // Own Properties
    this.pluralFunc = pluralFunc;
    this.locale = locale;
    this.fallbackLocale = fallbackLocale;
}

// Set up the locales object. Add in english by default
MessageFormat.locale = {
    "en": function(n) {
        if (n === 1) {
            return "one";
        }
        return "other";
    }
};

// Build out our basic SafeString type
// more or less stolen from Handlebars by @wycats
MessageFormat.SafeString = function(string) {
    this.string = string;
};

MessageFormat.SafeString.prototype.toString = function() {
    return this.string.toString();
};

MessageFormat.Utils = {
    numSub: function(string, key, depth) {
        // make sure that it's not an escaped octothorpe
        return string.replace(/^#|[^\\]#/g, function(m) {
            var prefix = m && m.length === 2 ? m.charAt(0) : '';
            return prefix + '" + (function(){ var x = ' +
                key + ';\nif( isNaN(x) ){\nthrow new Error("MessageFormat: `"+lastkey_' + depth + '+"` isnt a number.");\n}\nreturn x;\n})() + "';
        });
    },
    escapeExpression: function(string) {
        var escape = {
                "\n": "\\n",
                "\"": '\\"'
            },
            badChars = /[\n"]/g,
            possible = /[\n"]/,
            escapeChar = function(chr) {
                return escape[chr] || "&amp;";
            };

        // Don't escape SafeStrings, since they're already safe
        if (string instanceof MessageFormat.SafeString) {
            return string.toString();
        } else if (string === null || string === false) {
            return "";
        }

        if (!possible.test(string)) {
            return string;
        }
        return string.replace(badChars, escapeChar);
    },
    getFallbackLocale: function(locale) {
        var tagSeparator = locale.indexOf("-") >= 0 ? "-" : "_";

        // Lets just be friends, fallback through the language tags
        while (!MessageFormat.locale.hasOwnProperty(locale)) {
            locale = locale.substring(0, locale.lastIndexOf(tagSeparator));
            if (locale.length === 0) {
                return null;
            }
        }

        return locale;
    }
};

// This is generated and pulled in for browsers.
var mparser = (function() {
    /* Generated by PEG.js 0.6.2 (http://pegjs.majda.cz/). */

    var result = {
        /*
         * Parses the input with a generated parser. If the parsing is successfull,
         * returns a value explicitly or implicitly specified by the grammar from
         * which the parser was generated (see |PEG.buildParser|). If the parsing is
         * unsuccessful, throws |PEG.parser.SyntaxError| describing the error.
         */
        parse: function(input, startRule) {

            var pos = 0;
            var reportMatchFailures = true;
            var rightmostMatchFailuresPos = 0;
            var rightmostMatchFailuresExpected = [];
            var cache = {};
            var parse_elementFormat;

            function padLeft(input, padding, length) {
                var result = input;

                var padLength = length - input.length;
                for (var i = 0; i < padLength; i++) {
                    result = padding + result;
                }

                return result;
            }

            function escape(ch) {
                var length, escapeChar, 
                  charCode = ch.charCodeAt(0);

                if (charCode <= 0xFF) {
                    escapeChar = 'x';
                    length = 2;
                } else {
                    escapeChar = 'u';
                    length = 4;
                }

                return '\\' + escapeChar + padLeft(charCode.toString(16).toUpperCase(), '0', length);
            }

            function quote(s) {
                /*
                 * ECMA-262, 5th ed., 7.8.4: All characters may appear literally in a
                 * string literal except for the closing quote character, backslash,
                 * carriage return, line separator, paragraph separator, and line feed.
                 * Any character may appear in the form of an escape sequence.
                 */
                return '"' + s
                    .replace(/\\/g, '\\\\') // backslash
                    .replace(/"/g, '\\"') // closing quote character
                    .replace(/\r/g, '\\r') // carriage return
                    .replace(/\n/g, '\\n') // line feed
                    .replace(/[\x80-\uFFFF]/g, escape) + // non-ASCII characters
                    '"';
            }

            function matchFailed(failure) {
                if (pos < rightmostMatchFailuresPos) {
                    return;
                }

                if (pos > rightmostMatchFailuresPos) {
                    rightmostMatchFailuresPos = pos;
                    rightmostMatchFailuresExpected = [];
                }

                rightmostMatchFailuresExpected.push(failure);
            }

            function parse_whitespace() {
                var cacheKey = 'whitespace@' + pos;
                var cachedResult = cache[cacheKey];
                if (cachedResult) {
                    pos = cachedResult.nextPos;
                    return cachedResult.result;
                }

                var result0;
                if (input.substr(pos).match(/^[   \n\r]/) !== null) {
                    result0 = input.charAt(pos);
                    pos++;
                } else {
                    result0 = null;
                    if (reportMatchFailures) {
                        matchFailed("[  \\n\\r]");
                    }
                }

                cache[cacheKey] = {
                    nextPos: pos,
                    result: result0
                };
                return result0;
            }

            function parse__() {
                var cacheKey = '_@' + pos;
                var cachedResult = cache[cacheKey];
                if (cachedResult) {
                    pos = cachedResult.nextPos;
                    return cachedResult.result;
                }

                var savedReportMatchFailures = reportMatchFailures;
                reportMatchFailures = false;
                var savedPos0 = pos;
                var result1 = [];
                var result3 = parse_whitespace();
                while (result3 !== null) {
                    result1.push(result3);
                    result3 = parse_whitespace();
                }
                var result2 = result1 !== null ?
                    (function(w) {
                        return w.join('');
                    })(result1) :
                    null;

                var result0;
                if (result2 !== null) {
                    result0 = result2;
                } else {
                    result0 = null;
                    pos = savedPos0;
                }
                reportMatchFailures = savedReportMatchFailures;
                if (reportMatchFailures && result0 === null) {
                    matchFailed("whitespace");
                }

                cache[cacheKey] = {
                    nextPos: pos,
                    result: result0
                };
                return result0;
            }



            function parse_digits() {
                var cacheKey = 'digits@' + pos;
                var cachedResult = cache[cacheKey];
                if (cachedResult) {
                    pos = cachedResult.nextPos;
                    return cachedResult.result;
                }

                var savedPos0 = pos;
                var result3;
                if (input.substr(pos).match(/^[0-9]/) !== null) {
                    result3 = input.charAt(pos);
                    pos++;
                } else {
                    result3 = null;
                    if (reportMatchFailures) {
                        matchFailed("[0-9]");
                    }
                }

                var result1;
                if (result3 !== null) {
                    result1 = [];
                    while (result3 !== null) {
                        result1.push(result3);
                        if (input.substr(pos).match(/^[0-9]/) !== null) {
                            result3 = input.charAt(pos);
                            pos++;
                        } else {
                            result3 = null;
                            if (reportMatchFailures) {
                                matchFailed("[0-9]");
                            }
                        }
                    }
                } else {
                    result1 = null;
                }

                var result2 = result1 !== null ?
                    (function(ds) {
                        return parseInt((ds.join('')), 10);
                    })(result1) :
                    null;

                var result0;
                if (result2 !== null) {
                    result0 = result2;
                } else {
                    result0 = null;
                    pos = savedPos0;
                }

                cache[cacheKey] = {
                    nextPos: pos,
                    result: result0
                };
                return result0;
            }

            function parse_offsetPattern() {
                var cacheKey = 'offsetPattern@' + pos;
                var cachedResult = cache[cacheKey];
                if (cachedResult) {
                    pos = cachedResult.nextPos;
                    return cachedResult.result;
                }

                var savedPos0 = pos;
                var savedPos1 = pos;
                var result3 = parse__();
                var result1;
                if (result3 !== null) {
                    var result4;
                    if (input.substr(pos, 6) === "offset") {
                        result4 = "offset";
                        pos += 6;
                    } else {
                        result4 = null;
                        if (reportMatchFailures) {
                            matchFailed("\"offset\"");
                        }
                    }
                    if (result4 !== null) {
                        var result5 = parse__();
                        if (result5 !== null) {
                            var result6;
                            if (input.substr(pos, 1) === ":") {
                                result6 = ":";
                                pos += 1;
                            } else {
                                result6 = null;
                                if (reportMatchFailures) {
                                    matchFailed("\":\"");
                                }
                            }
                            if (result6 !== null) {
                                var result7 = parse__();
                                if (result7 !== null) {
                                    var result8 = parse_digits();
                                    if (result8 !== null) {
                                        var result9 = parse__();
                                        if (result9 !== null) {
                                            result1 = [result3, result4, result5, result6, result7, result8, result9];
                                        } else {
                                            result1 = null;
                                            pos = savedPos1;
                                        }
                                    } else {
                                        result1 = null;
                                        pos = savedPos1;
                                    }
                                } else {
                                    result1 = null;
                                    pos = savedPos1;
                                }
                            } else {
                                result1 = null;
                                pos = savedPos1;
                            }
                        } else {
                            result1 = null;
                            pos = savedPos1;
                        }
                    } else {
                        result1 = null;
                        pos = savedPos1;
                    }
                } else {
                    result1 = null;
                    pos = savedPos1;
                }
                var result2 = result1 !== null ?
                    (function(d) {
                        return d;
                    })(result1[5]) :
                    null;

                var result0;
                if (result2 !== null) {
                    result0 = result2;
                } else {
                    result0 = null;
                    pos = savedPos0;
                }

                cache[cacheKey] = {
                    nextPos: pos,
                    result: result0
                };
                return result0;
            }


            function parse_id() {
                var cacheKey = 'id@' + pos;
                var cachedResult = cache[cacheKey];
                if (cachedResult) {
                    pos = cachedResult.nextPos;
                    return cachedResult.result;
                }

                var savedPos0 = pos;
                var savedPos1 = pos;
                var result3 = parse__();
                var result1;
                if (result3 !== null) {
                    var result4;
                    if (input.substr(pos).match(/^[a-zA-Z$_]/) !== null) {
                        result4 = input.charAt(pos);
                        pos++;
                    } else {
                        result4 = null;
                        if (reportMatchFailures) {
                            matchFailed("[a-zA-Z$_]");
                        }
                    }
                    if (result4 !== null) {
                        var result5 = [];
                        var result7;
                        if (input.substr(pos).match(/^[^  \n\r,.+={}]/) !== null) {
                            result7 = input.charAt(pos);
                            pos++;
                        } else {
                            result7 = null;
                            if (reportMatchFailures) {
                                matchFailed("[^   \\n\\r,.+={}]");
                            }
                        }
                        while (result7 !== null) {
                            result5.push(result7);
                            if (input.substr(pos).match(/^[^  \n\r,.+={}]/) !== null) {
                                result7 = input.charAt(pos);
                                pos++;
                            } else {
                                result7 = null;
                                if (reportMatchFailures) {
                                    matchFailed("[^   \\n\\r,.+={}]");
                                }
                            }
                        }
                        if (result5 !== null) {
                            var result6 = parse__();
                            if (result6 !== null) {
                                result1 = [result3, result4, result5, result6];
                            } else {
                                result1 = null;
                                pos = savedPos1;
                            }
                        } else {
                            result1 = null;
                            pos = savedPos1;
                        }
                    } else {
                        result1 = null;
                        pos = savedPos1;
                    }
                } else {
                    result1 = null;
                    pos = savedPos1;
                }
                var result2 = result1 !== null ?
                    (function(s1, s2) {
                        return s1 + (s2 ? s2.join('') : '');
                    })(result1[1], result1[2]) :
                    null;

                var result0;
                if (result2 !== null) {
                    result0 = result2;
                } else {
                    result0 = null;
                    pos = savedPos0;
                }

                cache[cacheKey] = {
                    nextPos: pos,
                    result: result0
                };
                return result0;
            }

            function parse_stringKey() {
                var cacheKey = 'stringKey@' + pos;
                var cachedResult = cache[cacheKey];
                if (cachedResult) {
                    pos = cachedResult.nextPos;
                    return cachedResult.result;
                }

                var savedPos2 = pos;
                var result7 = parse_id();
                var result8 = result7 !== null ?
                    (function(i) {
                        return i;
                    })(result7) :
                    null;
                var result6;
                if (result8 !== null) {
                    result6 = result8;
                } else {
                    result6 = null;
                    pos = savedPos2;
                }
                var result0;
                if (result6 !== null) {
                    result0 = result6;
                } else {
                    var savedPos0 = pos;
                    var savedPos1 = pos;
                    var result4;
                    if (input.substr(pos, 1) === "=") {
                        result4 = "=";
                        pos += 1;
                    } else {
                        result4 = null;
                        if (reportMatchFailures) {
                            matchFailed("\"=\"");
                        }
                    }
                    var result2;
                    if (result4 !== null) {
                        var result5 = parse_digits();
                        if (result5 !== null) {
                            result2 = [result4, result5];
                        } else {
                            result2 = null;
                            pos = savedPos1;
                        }
                    } else {
                        result2 = null;
                        pos = savedPos1;
                    }
                    var result3 = result2 !== null ?
                        (function(d) {
                            return d;
                        })(result2[1]) :
                        null;

                    var result1;
                    if (result3 !== null) {
                        result1 = result3;
                    } else {
                        result1 = null;
                        pos = savedPos0;
                    }
                    if (result1 !== null) {
                        result0 = result1;
                    } else {
                        result0 = null;
                    }
                }

                cache[cacheKey] = {
                    nextPos: pos,
                    result: result0
                };

                return result0;
            }

            function parse_hexDigit() {
                var cacheKey = 'hexDigit@' + pos;
                var cachedResult = cache[cacheKey];
                if (cachedResult) {
                    pos = cachedResult.nextPos;
                    return cachedResult.result;
                }

                var result0;
                if (input.substr(pos).match(/^[0-9a-fA-F]/) !== null) {
                    result0 = input.charAt(pos);
                    pos++;
                } else {
                    result0 = null;
                    if (reportMatchFailures) {
                        matchFailed("[0-9a-fA-F]");
                    }
                }

                cache[cacheKey] = {
                    nextPos: pos,
                    result: result0
                };
                return result0;
            }

            var charPattern = "^[^{}\\\0-     \n\r]", // jshint ignore:line
                charPatternMatch = new RegExp("/" + charPattern + "/");
            function parse_char() {
                var cacheKey = 'char@' + pos;
                var cachedResult = cache[cacheKey];

                if (cachedResult) {
                    pos = cachedResult.nextPos;
                    return cachedResult.result;
                }

                var savedPos5 = pos;
                var result19;
                if (input.substr(pos).match(charPattern) !== null) {
                    result19 = input.charAt(pos);
                    pos++;
                } else {
                    result19 = null;
                    if (reportMatchFailures) {
                        matchFailed(charPattern);
                    }
                }

                var result20 = result19 !== null ?
                    (function(x) {
                        return x;
                    })(result19) :
                    null;

                var result18;
                if (result20 !== null) {
                    result18 = result20;
                } else {
                    result18 = null;
                    pos = savedPos5;
                }

                var result0;
                if (result18 !== null) {
                    result0 = result18;
                } else {
                    var savedPos4 = pos;
                    var result16;
                    if (input.substr(pos, 2) === "\\#") {
                        result16 = "\\#";
                        pos += 2;
                    } else {
                        result16 = null;
                        if (reportMatchFailures) {
                            matchFailed("\"\\\\#\"");
                        }
                    }
                    var result17 = result16 !== null ?
                        (function() {
                            return "\\#";
                        })() :
                        null;
                    var result15;
                    if (result17 !== null) {
                        result15 = result17;
                    } else {
                        result15 = null;
                        pos = savedPos4;
                    }
                    if (result15 !== null) {
                        result0 = result15;
                    } else {
                        var savedPos3 = pos;
                        var result13;
                        if (input.substr(pos, 2) === "\\{") {
                            result13 = "\\{";
                            pos += 2;
                        } else {
                            result13 = null;
                            if (reportMatchFailures) {
                                matchFailed("\"\\\\{\"");
                            }
                        }
                        var result14 = result13 !== null ?
                            (function() {
                                return "\u007B";
                            })() :
                            null;
                        var result12;
                        if (result14 !== null) {
                            result12 = result14;
                        } else {
                            result12 = null;
                            pos = savedPos3;
                        }
                        if (result12 !== null) {
                            result0 = result12;
                        } else {
                            var savedPos2 = pos;
                            var result10;
                            if (input.substr(pos, 2) === "\\}") {
                                result10 = "\\}";
                                pos += 2;
                            } else {
                                result10 = null;
                                if (reportMatchFailures) {
                                    matchFailed("\"\\\\}\"");
                                }
                            }
                            var result11 = result10 !== null ?
                                (function() {
                                    return "\u007D";
                                })() :
                                null;

                            var result9;
                            if (result11 !== null) {
                                result9 = result11;
                            } else {
                                result9 = null;
                                pos = savedPos2;
                            }
                            if (result9 !== null) {
                                result0 = result9;
                            } else {
                                var savedPos0 = pos;
                                var savedPos1 = pos;
                                var result4;
                                if (input.substr(pos, 2) === "\\u") {
                                    result4 = "\\u";
                                    pos += 2;
                                } else {
                                    result4 = null;
                                    if (reportMatchFailures) {
                                        matchFailed("\"\\\\u\"");
                                    }
                                }
                                var result2;
                                if (result4 !== null) {
                                    var result5 = parse_hexDigit();
                                    if (result5 !== null) {
                                        var result6 = parse_hexDigit();
                                        if (result6 !== null) {
                                            var result7 = parse_hexDigit();
                                            if (result7 !== null) {
                                                var result8 = parse_hexDigit();
                                                if (result8 !== null) {
                                                    result2 = [result4, result5, result6, result7, result8];
                                                } else {
                                                    result2 = null;
                                                    pos = savedPos1;
                                                }
                                            } else {
                                                result2 = null;
                                                pos = savedPos1;
                                            }
                                        } else {
                                            result2 = null;
                                            pos = savedPos1;
                                        }
                                    } else {
                                        result2 = null;
                                        pos = savedPos1;
                                    }
                                } else {
                                    result2 = null;
                                    pos = savedPos1;
                                }

                                var result3 = result2 !== null ?
                                    (function(h1, h2, h3, h4) {
                                        return String.fromCharCode(parseInt("0x" + h1 + h2 + h3 + h4));
                                    })(result2[1], result2[2], result2[3], result2[4]) :
                                    null;

                                var result1;
                                if (result3 !== null) {
                                    result1 = result3;
                                } else {
                                    result1 = null;
                                    pos = savedPos0;
                                }
                                if (result1 !== null) {
                                    result0 = result1;
                                } else {
                                    result0 = null;
                                }
                            }
                        }
                    }
                }

                cache[cacheKey] = {
                    nextPos: pos,
                    result: result0
                };

                return result0;
            }

            function parse_chars() {
                var cacheKey = 'chars@' + pos;
                var cachedResult = cache[cacheKey];
                if (cachedResult) {
                    pos = cachedResult.nextPos;
                    return cachedResult.result;
                }


                var savedPos0 = pos;
                var result3 = parse_char();
                var result1;
                if (result3 !== null) {
                    result1 = [];
                    while (result3 !== null) {
                        result1.push(result3);
                        result3 = parse_char();
                    }
                } else {
                    result1 = null;
                }
                var result2 = result1 !== null ?
                    (function(chars) {
                        return chars.join('');
                    })(result1) :
                    null;

                var result0;
                if (result2 !== null) {
                    result0 = result2;
                } else {
                    result0 = null;
                    pos = savedPos0;
                }

                cache[cacheKey] = {
                    nextPos: pos,
                    result: result0
                };
                return result0;
            }

            function parse_string() {
                var cacheKey = 'string@' + pos;
                var cachedResult = cache[cacheKey];
                if (cachedResult) {
                    pos = cachedResult.nextPos;
                    return cachedResult.result;
                }

                var savedPos0 = pos;
                var savedPos1 = pos;
                var result3 = parse__();
                var result1;
                if (result3 !== null) {
                    var result4 = [];
                    var savedPos2 = pos;
                    var result6 = parse__();
                    var result8;
                    var result5;
                    var result7;
                    if (result6 !== null) {
                        result7 = parse_chars();
                        if (result7 !== null) {
                            result8 = parse__();
                            if (result8 !== null) {
                                result5 = [result6, result7, result8];
                            } else {
                                result5 = null;
                                pos = savedPos2;
                            }
                        } else {
                            result5 = null;
                            pos = savedPos2;
                        }
                    } else {
                        result5 = null;
                        pos = savedPos2;
                    }
                    while (result5 !== null) {
                        result4.push(result5);
                        savedPos2 = pos;
                        result6 = parse__();
                        if (result6 !== null) {
                            result7 = parse_chars();
                            if (result7 !== null) {
                                result8 = parse__();
                                if (result8 !== null) {
                                    result5 = [result6, result7, result8];
                                } else {
                                    result5 = null;
                                    pos = savedPos2;
                                }
                            } else {
                                result5 = null;
                                pos = savedPos2;
                            }
                        } else {
                            result5 = null;
                            pos = savedPos2;
                        }
                    }
                    if (result4 !== null) {
                        result1 = [result3, result4];
                    } else {
                        result1 = null;
                        pos = savedPos1;
                    }
                } else {
                    result1 = null;
                    pos = savedPos1;
                }
                var result2 = result1 !== null ?
                    (function(ws, s) {
                        var tmp = [];
                        for (var i = 0; i < s.length; ++i) {
                            for (var j = 0; j < s[i].length; ++j) {
                                tmp.push(s[i][j]);
                            }
                        }
                        return {
                            type: "string",
                            val: ws + tmp.join('')
                        };
                    })(result1[0], result1[1]) :
                    null;

                var result0;
                if (result2 !== null) {
                    result0 = result2;
                } else {
                    result0 = null;
                    pos = savedPos0;
                }

                cache[cacheKey] = {
                    nextPos: pos,
                    result: result0
                };
                return result0;
            }

            

            function parse_messageFormatElement() {
                var cacheKey = 'messageFormatElement@' + pos;
                var cachedResult = cache[cacheKey];
                if (cachedResult) {
                    pos = cachedResult.nextPos;
                    return cachedResult.result;
                }

                var savedPos0 = pos;
                var savedPos1 = pos;
                var result3 = parse_id();
                var result1;
                if (result3 !== null) {
                    var savedPos2 = pos;
                    var result6;
                    if (input.substr(pos, 1) === ",") {
                        result6 = ",";
                        pos += 1;
                    } else {
                        result6 = null;
                        if (reportMatchFailures) {
                            matchFailed("\",\"");
                        }
                    }
                    var result5;
                    if (result6 !== null) {
                        var result7 = parse_elementFormat();
                        if (result7 !== null) {
                            result5 = [result6, result7];
                        } else {
                            result5 = null;
                            pos = savedPos2;
                        }
                    } else {
                        result5 = null;
                        pos = savedPos2;
                    }
                    var result4 = result5 !== null ? result5 : '';
                    if (result4 !== null) {
                        result1 = [result3, result4];
                    } else {
                        result1 = null;
                        pos = savedPos1;
                    }
                } else {
                    result1 = null;
                    pos = savedPos1;
                }
                var result2 = result1 !== null ?
                    (function(argIdx, efmt) {
                        var res = {
                            type: "messageFormatElement",
                            argumentIndex: argIdx
                        };
                        if (efmt && efmt.length) {
                            res.elementFormat = efmt[1];
                        } else {
                            res.output = true;
                        }
                        return res;
                    })(result1[0], result1[1]) :
                    null;

                var result0;
                if (result2 !== null) {
                    result0 = result2;
                } else {
                    result0 = null;
                    pos = savedPos0;
                }

                cache[cacheKey] = {
                    nextPos: pos,
                    result: result0
                };
                return result0;
            }

            function parse_messageFormatPatternRight() {
                var cacheKey = 'messageFormatPatternRight@' + pos;
                var cachedResult = cache[cacheKey];
                if (cachedResult) {
                    pos = cachedResult.nextPos;
                    return cachedResult.result;
                }


                var savedPos0 = pos;
                var savedPos1 = pos;
                var result3;
                if (input.substr(pos, 1) === "{") {
                    result3 = "{";
                    pos += 1;
                } else {
                    result3 = null;
                    if (reportMatchFailures) {
                        matchFailed("\"{\"");
                    }
                }
                var result1;
                if (result3 !== null) {
                    var result4 = parse__();
                    if (result4 !== null) {
                        var result5 = parse_messageFormatElement();
                        if (result5 !== null) {
                            var result6 = parse__();
                            if (result6 !== null) {
                                var result7;
                                if (input.substr(pos, 1) === "}") {
                                    result7 = "}";
                                    pos += 1;
                                } else {
                                    result7 = null;
                                    if (reportMatchFailures) {
                                        matchFailed("\"}\"");
                                    }
                                }
                                if (result7 !== null) {
                                    var result8 = parse_string();
                                    if (result8 !== null) {
                                        result1 = [result3, result4, result5, result6, result7, result8];
                                    } else {
                                        result1 = null;
                                        pos = savedPos1;
                                    }
                                } else {
                                    result1 = null;
                                    pos = savedPos1;
                                }
                            } else {
                                result1 = null;
                                pos = savedPos1;
                            }
                        } else {
                            result1 = null;
                            pos = savedPos1;
                        }
                    } else {
                        result1 = null;
                        pos = savedPos1;
                    }
                } else {
                    result1 = null;
                    pos = savedPos1;
                }
                var result2 = result1 !== null ?
                    (function(mfe, s1) {
                        var res = [];
                        if (mfe) {
                            res.push(mfe);
                        }
                        if (s1 && s1.val) {
                            res.push(s1);
                        }
                        return {
                            type: "messageFormatPatternRight",
                            statements: res
                        };
                    })(result1[2], result1[5]) :
                    null;

                var result0;
                if (result2 !== null) {
                    result0 = result2;
                } else {
                    result0 = null;
                    pos = savedPos0;
                }

                cache[cacheKey] = {
                    nextPos: pos,
                    result: result0
                };
                return result0;
            }

            function parse_messageFormatPattern() {
                var cacheKey = 'messageFormatPattern@' + pos;
                var cachedResult = cache[cacheKey];
                if (cachedResult) {
                    pos = cachedResult.nextPos;
                    return cachedResult.result;
                }

                var savedPos0 = pos;
                var savedPos1 = pos;
                var result1;
                var result3 = parse_string();
                if (result3 !== null) {
                    var result4 = [];
                    var result5 = parse_messageFormatPatternRight();
                    while (result5 !== null) {
                        result4.push(result5);
                        result5 = parse_messageFormatPatternRight();
                    }
                    if (result4 !== null) {
                        result1 = [result3, result4];
                    } else {
                        result1 = null;
                        pos = savedPos1;
                    }
                } else {
                    result1 = null;
                    pos = savedPos1;
                }
                var result0;
                var result2 = result1 !== null ?
                    (function(s1, inner) {
                        var st = [];
                        if (s1 && s1.val) {
                            st.push(s1);
                        }
                        for (var i in inner) {
                            if (inner.hasOwnProperty(i)) {
                                st.push(inner[i]);
                            }
                        }
                        return {
                            type: 'messageFormatPattern',
                            statements: st
                        };
                    })(result1[0], result1[1]) :
                    null;
                if (result2 !== null) {
                    result0 = result2;
                } else {
                    result0 = null;
                    pos = savedPos0;
                }

                cache[cacheKey] = {
                    nextPos: pos,
                    result: result0
                };
                return result0;
            }

            function parse_pluralForms() {
                var cacheKey = 'pluralForms@' + pos;
                var cachedResult = cache[cacheKey];
                if (cachedResult) {
                    pos = cachedResult.nextPos;
                    return cachedResult.result;
                }

                var savedPos0 = pos;
                var savedPos1 = pos;
                var result1;
                var result3 = parse__();
                if (result3 !== null) {
                    var result4 = parse_stringKey();
                    if (result4 !== null) {
                        var result5 = parse__();
                        if (result5 !== null) {
                            var result6;
                            if (input.substr(pos, 1) === "{") {
                                result6 = "{";
                                pos += 1;
                            } else {
                                result6 = null;
                                if (reportMatchFailures) {
                                    matchFailed("\"{\"");
                                }
                            }
                            if (result6 !== null) {
                                var result7 = parse__();
                                if (result7 !== null) {
                                    var result8 = parse_messageFormatPattern();
                                    if (result8 !== null) {
                                        var result9 = parse__();
                                        if (result9 !== null) {
                                            var result10;
                                            if (input.substr(pos, 1) === "}") {
                                                result10 = "}";
                                                pos += 1;
                                            } else {
                                                result10 = null;
                                                if (reportMatchFailures) {
                                                    matchFailed("\"}\"");
                                                }
                                            }
                                            if (result10 !== null) {
                                                result1 = [result3, result4, result5, result6, result7, result8, result9, result10];
                                            } else {
                                                result1 = null;
                                                pos = savedPos1;
                                            }
                                        } else {
                                            result1 = null;
                                            pos = savedPos1;
                                        }
                                    } else {
                                        result1 = null;
                                        pos = savedPos1;
                                    }
                                } else {
                                    result1 = null;
                                    pos = savedPos1;
                                }
                            } else {
                                result1 = null;
                                pos = savedPos1;
                            }
                        } else {
                            result1 = null;
                            pos = savedPos1;
                        }
                    } else {
                        result1 = null;
                        pos = savedPos1;
                    }
                } else {
                    result1 = null;
                    pos = savedPos1;
                }

                var result2 = result1 !== null ?
                    (function(k, mfp) {
                        return {
                            type: "pluralForms",
                            key: k,
                            val: mfp
                        };
                    })(result1[1], result1[5]) :
                    null;

                var result0;
                if (result2 !== null) {
                    result0 = result2;
                } else {
                    result0 = null;
                    pos = savedPos0;
                }

                cache[cacheKey] = {
                    nextPos: pos,
                    result: result0
                };
                return result0;
            }

            function parse_selectFormatPattern() {
                var cacheKey = 'selectFormatPattern@' + pos;
                var cachedResult = cache[cacheKey];
                if (cachedResult) {
                    pos = cachedResult.nextPos;
                    return cachedResult.result;
                }


                var savedPos0 = pos;
                var result1 = [];
                var result3 = parse_pluralForms();
                while (result3 !== null) {
                    result1.push(result3);
                    result3 = parse_pluralForms();
                }
                var result2 = result1 !== null ?
                    (function(pf) {
                        return {
                            type: "selectFormatPattern",
                            pluralForms: pf
                        };
                    })(result1) :
                    null;

                var result0;
                if (result2 !== null) {
                    result0 = result2;
                } else {
                    result0 = null;
                    pos = savedPos0;
                }

                cache[cacheKey] = {
                    nextPos: pos,
                    result: result0
                };
                return result0;
            }

            function parse_selectStyle() {
                var cacheKey = 'selectStyle@' + pos;
                var cachedResult = cache[cacheKey];
                if (cachedResult) {
                    pos = cachedResult.nextPos;
                    return cachedResult.result;
                }

                var savedPos0 = pos;
                var result1 = parse_selectFormatPattern();
                var result2 = result1 !== null ?
                    (function(sfp) {
                        return {
                            type: "selectStyle",
                            val: sfp
                        };
                    })(result1) :
                    null;

                var result0;
                if (result2 !== null) {
                    result0 = result2;
                } else {
                    result0 = null;
                    pos = savedPos0;
                }

                cache[cacheKey] = {
                    nextPos: pos,
                    result: result0
                };
                return result0;
            }

            function parse_pluralFormatPattern() {
                var cacheKey = 'pluralFormatPattern@' + pos;
                var cachedResult = cache[cacheKey];
                if (cachedResult) {
                    pos = cachedResult.nextPos;
                    return cachedResult.result;
                }

                var savedPos0 = pos;
                var savedPos1 = pos;
                var result6 = parse_offsetPattern();
                var result3 = result6 !== null ? result6 : '';
                var result1;
                if (result3 !== null) {
                    var result4 = [];
                    var result5 = parse_pluralForms();
                    while (result5 !== null) {
                        result4.push(result5);
                        result5 = parse_pluralForms();
                    }
                    if (result4 !== null) {
                        result1 = [result3, result4];
                    } else {
                        result1 = null;
                        pos = savedPos1;
                    }
                } else {
                    result1 = null;
                    pos = savedPos1;
                }
                var result2 = result1 !== null ?
                    (function(op, pf) {
                        var res = {
                            type: "pluralFormatPattern",
                            pluralForms: pf
                        };
                        if (op) {
                            res.offset = op;
                        } else {
                            res.offset = 0;
                        }
                        return res;
                    })(result1[0], result1[1]) :
                    null;
                
                var result0;
                if (result2 !== null) {
                    result0 = result2;
                } else {
                    result0 = null;
                    pos = savedPos0;
                }

                cache[cacheKey] = {
                    nextPos: pos,
                    result: result0
                };
                
                return result0;
            }

            function parse_pluralStyle() {
                var cacheKey = 'pluralStyle@' + pos;
                var cachedResult = cache[cacheKey];
                if (cachedResult) {
                    pos = cachedResult.nextPos;
                    return cachedResult.result;
                }

                var savedPos0 = pos;
                var result1 = parse_pluralFormatPattern();
                var result2 = result1 !== null ?
                    (function(pfp) {
                        return {
                            type: "pluralStyle",
                            val: pfp
                        };
                    })(result1) :
                    null;

                var result0;
                if (result2 !== null) {
                    result0 = result2;
                } else {
                    result0 = null;
                    pos = savedPos0;
                }

                cache[cacheKey] = {
                    nextPos: pos,
                    result: result0
                };
                return result0;
            }

            parse_elementFormat = function () {
                var cacheKey = 'elementFormat@' + pos;
                var cachedResult = cache[cacheKey];
                if (cachedResult) {
                    pos = cachedResult.nextPos;
                    return cachedResult.result;
                }


                var savedPos2 = pos;
                var savedPos3 = pos;
                var result14 = parse__();
                var result12;
                if (result14 !== null) {
                    var result15;
                    if (input.substr(pos, 6) === "plural") {
                        result15 = "plural";
                        pos += 6;
                    } else {
                        result15 = null;
                        if (reportMatchFailures) {
                            matchFailed("\"plural\"");
                        }
                    }
                    if (result15 !== null) {
                        var result16 = parse__();
                        if (result16 !== null) {
                            var result17;
                            if (input.substr(pos, 1) === ",") {
                                result17 = ",";
                                pos += 1;
                            } else {
                                result17 = null;
                                if (reportMatchFailures) {
                                    matchFailed("\",\"");
                                }
                            }
                            if (result17 !== null) {
                                var result18 = parse__();
                                if (result18 !== null) {
                                    var result19 = parse_pluralStyle();
                                    if (result19 !== null) {
                                        var result20 = parse__();
                                        if (result20 !== null) {
                                            result12 = [result14, result15, result16, result17, result18, result19, result20];
                                        } else {
                                            result12 = null;
                                            pos = savedPos3;
                                        }
                                    } else {
                                        result12 = null;
                                        pos = savedPos3;
                                    }
                                } else {
                                    result12 = null;
                                    pos = savedPos3;
                                }
                            } else {
                                result12 = null;
                                pos = savedPos3;
                            }
                        } else {
                            result12 = null;
                            pos = savedPos3;
                        }
                    } else {
                        result12 = null;
                        pos = savedPos3;
                    }
                } else {
                    result12 = null;
                    pos = savedPos3;
                }
                var result13 = result12 !== null ?
                    (function(t, s) {
                        return {
                            type: "elementFormat",
                            key: t,
                            val: s.val
                        };
                    })(result12[1], result12[5]) :
                    null;

                var result11;
                if (result13 !== null) {
                    result11 = result13;
                } else {
                    result11 = null;
                    pos = savedPos2;
                }

                var result0;
                var result2;
                if (result11 !== null) {
                    result0 = result11;
                } else {
                    var savedPos0 = pos;
                    var savedPos1 = pos;
                    var result4 = parse__();
                    if (result4 !== null) {
                        var result5;
                        if (input.substr(pos, 6) === "select") {
                            result5 = "select";
                            pos += 6;
                        } else {
                            result5 = null;
                            if (reportMatchFailures) {
                                matchFailed("\"select\"");
                            }
                        }
                        if (result5 !== null) {
                            var result6 = parse__();
                            if (result6 !== null) {
                                var result7;
                                if (input.substr(pos, 1) === ",") {
                                    result7 = ",";
                                    pos += 1;
                                } else {
                                    result7 = null;
                                    if (reportMatchFailures) {
                                        matchFailed("\",\"");
                                    }
                                }
                                if (result7 !== null) {
                                    var result8 = parse__();
                                    if (result8 !== null) {
                                        var result9 = parse_selectStyle();
                                        if (result9 !== null) {
                                            var result10 = parse__();
                                            if (result10 !== null) {
                                                result2 = [result4, result5, result6, result7, result8, result9, result10];
                                            } else {
                                                result2 = null;
                                                pos = savedPos1;
                                            }
                                        } else {
                                            result2 = null;
                                            pos = savedPos1;
                                        }
                                    } else {
                                        result2 = null;
                                        pos = savedPos1;
                                    }
                                } else {
                                    result2 = null;
                                    pos = savedPos1;
                                }
                            } else {
                                result2 = null;
                                pos = savedPos1;
                            }
                        } else {
                            result2 = null;
                            pos = savedPos1;
                        }
                    } else {
                        result2 = null;
                        pos = savedPos1;
                    }
                    var result3 = result2 !== null ?
                        (function(t, s) {
                            return {
                                type: "elementFormat",
                                key: t,
                                val: s.val
                            };
                        })(result2[1], result2[5]) :
                        null;

                    var result1;
                    if (result3 !== null) {
                        result1 = result3;
                    } else {
                        result1 = null;
                        pos = savedPos0;
                    }
                    if (result1 !== null) {
                        result0 = result1;
                    } else {
                        result0 = null;
                    }
                }

                cache[cacheKey] = {
                    nextPos: pos,
                    result: result0
                };
                return result0;
            };

            function parse_start() {
                var cacheKey = 'start@' + pos;
                var cachedResult = cache[cacheKey];
                if (cachedResult) {
                    pos = cachedResult.nextPos;
                    return cachedResult.result;
                }


                var savedPos0 = pos;
                var result0;
                var result1 = parse_messageFormatPattern();
                var result2 = result1 !== null ?
                    (function(messageFormatPattern) {
                        return {
                            type: "program",
                            program: messageFormatPattern
                        };
                    })(result1) :
                    null;
                if (result2 !== null) {
                    result0 = result2;
                } else {
                    result0 = null;
                    pos = savedPos0;
                }

                cache[cacheKey] = {
                    nextPos: pos,
                    result: result0
                };
                return result0;
            }

            function buildErrorMessage() {
                function buildExpected(failuresExpected) {
                    failuresExpected.sort();

                    var lastFailure = null;
                    var failuresExpectedUnique = [];
                    for (var i = 0; i < failuresExpected.length; i++) {
                        if (failuresExpected[i] !== lastFailure) {
                            failuresExpectedUnique.push(failuresExpected[i]);
                            lastFailure = failuresExpected[i];
                        }
                    }

                    switch (failuresExpectedUnique.length) {
                        case 0:
                            return 'end of input';
                        case 1:
                            return failuresExpectedUnique[0];
                        default:
                            return failuresExpectedUnique.slice(0, failuresExpectedUnique.length - 1).join(', ') +
                                ' or ' +
                                failuresExpectedUnique[failuresExpectedUnique.length - 1];
                    }
                }

                var expected = buildExpected(rightmostMatchFailuresExpected);
                var actualPos = Math.max(pos, rightmostMatchFailuresPos);
                var actual = actualPos < input.length ?
                    quote(input.charAt(actualPos)) :
                    'end of input';

                return 'Expected ' + expected + ' but ' + actual + ' found.';
            }

            function computeErrorPosition() {
                /*
                 * The first idea was to use |String.split| to break the input up to the
                 * error position along newlines and derive the line and column from
                 * there. However IE's |split| implementation is so broken that it was
                 * enough to prevent it.
                 */

                var line = 1;
                var column = 1;
                var seenCR = false;

                for (var i = 0; i < rightmostMatchFailuresPos; i++) {
                    var ch = input.charAt(i);
                    if (ch === '\n') {
                        if (!seenCR) {
                            line++;
                        }
                        column = 1;
                        seenCR = false;
                    } else if (ch === '\r' | ch === '\u2028' || ch === '\u2029') {
                        line++;
                        column = 1;
                        seenCR = true;
                    } else {
                        column++;
                        seenCR = false;
                    }
                }

                return {
                    line: line,
                    column: column
                };
            }

            var parseFunctions = {
                "_": parse__,
                "char": parse_char,
                "chars": parse_chars,
                "digits": parse_digits,
                "elementFormat": parse_elementFormat,
                "hexDigit": parse_hexDigit,
                "id": parse_id,
                "messageFormatElement": parse_messageFormatElement,
                "messageFormatPattern": parse_messageFormatPattern,
                "messageFormatPatternRight": parse_messageFormatPatternRight,
                "offsetPattern": parse_offsetPattern,
                "pluralFormatPattern": parse_pluralFormatPattern,
                "pluralForms": parse_pluralForms,
                "pluralStyle": parse_pluralStyle,
                "selectFormatPattern": parse_selectFormatPattern,
                "selectStyle": parse_selectStyle,
                "start": parse_start,
                "string": parse_string,
                "stringKey": parse_stringKey,
                "whitespace": parse_whitespace
            };

            if (startRule !== undefined) {
                if (parseFunctions[startRule] === undefined) {
                    throw new Error("Invalid rule name: " + quote(startRule) + ".");
                }
            } else {
                startRule = "start";
            }

            
            var result = parseFunctions[startRule]();

            /*
             * The parser is now in one of the following three states:
             *
             * 1. The parser successfully parsed the whole input.
             *
             *    - |result !== null|
             *    - |pos === input.length|
             *    - |rightmostMatchFailuresExpected| may or may not contain something
             *
             * 2. The parser successfully parsed only a part of the input.
             *
             *    - |result !== null|
             *    - |pos < input.length|
             *    - |rightmostMatchFailuresExpected| may or may not contain something
             *
             * 3. The parser did not successfully parse any part of the input.
             *
             *   - |result === null|
             *   - |pos === 0|
             *   - |rightmostMatchFailuresExpected| contains at least one failure
             *
             * All code following this comment (including called functions) must
             * handle these states.
             */
            if (result === null || pos !== input.length) {
                var errorPosition = computeErrorPosition();
                throw new this.SyntaxError(
                    buildErrorMessage(),
                    errorPosition.line,
                    errorPosition.column
                );
            }

            return result;
        },

        /* Returns the parser source code. */
        toSource: function() {
            return this._source;
        }
    };

    /* Thrown when a parser encounters a syntax error. */

    result.SyntaxError = function(message, line, column) {
        this.name = 'SyntaxError';
        this.message = message;
        this.line = line;
        this.column = column;
    };

    result.SyntaxError.prototype = Error.prototype;

    return result;
})();

MessageFormat.prototype.parse = function() {
    // Bind to itself so error handling works
    return mparser.parse.apply(mparser, arguments);
};

MessageFormat.prototype.precompile = function(ast) {
    var self = this,
        needOther = false,
        fp = {
            begin: 'function(d){\nvar r = "";\n',
            end: "return r;\n}"
        };

    function interpMFP(ast, data) {
        // Set some default data
        data = data || {};
        var s = '',
            res, i, tmp, lastkeyname;

        switch (ast.type) {
            case 'program':
                return interpMFP(ast.program);
            case 'messageFormatPattern':
                for (i = 0; i < ast.statements.length; ++i) {
                    s += interpMFP(ast.statements[i], data);
                }
                return fp.begin + s + fp.end;
            case 'messageFormatPatternRight':
                for (i = 0; i < ast.statements.length; ++i) {
                    s += interpMFP(ast.statements[i], data);
                }
                return s;
            case 'messageFormatElement':
                data.pf_count = data.pf_count || 0;
                s += 'if(!d){\nthrow new Error("MessageFormat: No data passed to function.");\n}\n';
                if (ast.output) {
                    s += 'r += d["' + ast.argumentIndex + '"];\n';
                } else {
                    lastkeyname = 'lastkey_' + (data.pf_count + 1);
                    s += 'var ' + lastkeyname + ' = "' + ast.argumentIndex + '";\n';
                    s += 'var k_' + (data.pf_count + 1) + '=d[' + lastkeyname + '];\n';
                    s += interpMFP(ast.elementFormat, data);
                }
                return s;
            case 'elementFormat':
                if (ast.key === 'select') {
                    s += interpMFP(ast.val, data);
                    s += 'r += (pf_' +
                        data.pf_count +
                        '[ k_' + (data.pf_count + 1) + ' ] || pf_' + data.pf_count + '[ "other" ])( d );\n';
                } else if (ast.key === 'plural') {
                    s += interpMFP(ast.val, data);
                    s += 'if ( pf_' + (data.pf_count) + '[ k_' + (data.pf_count + 1) + ' + "" ] ) {\n';
                    s += 'r += pf_' + data.pf_count + '[ k_' + (data.pf_count + 1) + ' + "" ]( d ); \n';
                    s += '}\nelse {\n';
                    s += 'r += (pf_' +
                        data.pf_count +
                        '[ MessageFormat.locale["' +
                        self.fallbackLocale +
                        '"]( k_' + (data.pf_count + 1) + ' - off_' + (data.pf_count) + ' ) ] || pf_' + data.pf_count + '[ "other" ] )( d );\n';
                    s += '}\n';
                }
                return s;
                /* // Unreachable cases.
                case 'pluralStyle':
                case 'selectStyle':*/
            case 'pluralFormatPattern':
                data.pf_count = data.pf_count || 0;
                s += 'var off_' + data.pf_count + ' = ' + ast.offset + ';\n';
                s += 'var pf_' + data.pf_count + ' = { \n';
                needOther = true;
                // We're going to simultaneously check to make sure we hit the required 'other' option.

                for (i = 0; i < ast.pluralForms.length; ++i) {
                    if (ast.pluralForms[i].key === 'other') {
                        needOther = false;
                    }
                    if (tmp) {
                        s += ',\n';
                    } else {
                        tmp = 1;
                    }
                    res = JSON.parse(JSON.stringify(data));
                    res.pf_count++;
                    s += '"' + ast.pluralForms[i].key + '" : ' + interpMFP(ast.pluralForms[i].val, res);
                }
                s += '\n};\n';
                if (needOther) {
                    throw new Error("No 'other' form found in pluralFormatPattern " + data.pf_count);
                }
                return s;
            case 'selectFormatPattern':

                data.pf_count = data.pf_count || 0;
                s += 'var off_' + data.pf_count + ' = 0;\n';
                s += 'var pf_' + data.pf_count + ' = { \n';
                needOther = true;

                for (i = 0; i < ast.pluralForms.length; ++i) {
                    if (ast.pluralForms[i].key === 'other') {
                        needOther = false;
                    }
                    if (tmp) {
                        s += ',\n';
                    } else {
                        tmp = 1;
                    }
                    res = JSON.parse(JSON.stringify(data));
                    res.pf_count++;
                    s += '"' + ast.pluralForms[i].key + '" : ' + interpMFP(ast.pluralForms[i].val, res);
                }
                s += '\n};\n';
                if (needOther) {
                    throw new Error("No 'other' form found in selectFormatPattern " + data.pf_count);
                }
                return s;
                /* // Unreachable
                case 'pluralForms':
                */
            case 'string':
                return 'r += "' + MessageFormat.Utils.numSub(
                    MessageFormat.Utils.escapeExpression(ast.val),
                    'k_' + data.pf_count + ' - off_' + (data.pf_count - 1),
                    data.pf_count
                ) + '";\n';
            default:
                throw new Error('Bad AST type: ' + ast.type);
        }
    }
    return interpMFP(ast);
};

MessageFormat.prototype.compile = function(message) {
    /* jshint evil: true */
    return new Function('MessageFormat',
        'return ' +
        this.precompile(
            this.parse(message)
        )
    )(MessageFormat);
    /* jshint evil: false */
};

module.exports = MessageFormat;